﻿using System;
using System.Data;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.ServiceModel.Syndication;

namespace Microsoft.SqlCommunity.SQLRSS
{
    /// <summary>
    /// This class represents a particular way of accessing a particular database on a particular server with
    /// particular authentication information.  It contains a cache of metadata information about the
    /// database. In particular it contains information about the tables in the database.  This class is designed
    /// to be accessed from multiple threads and is designed to serialize incompatible access while permitting
    /// as much parallelism as possible.
    /// </summary>
    public class Connection : Description
    { 
        //The server name, database name, and authentication information represented in a string
        private string _ConnectionString = null;
        //The ADO.NET object which represents a conduit to a particular database on a particular server.
        private SqlConnection _conn = null;

        //Contains the set of tables used to provide additional information related to tables being monitored for changes
        public ProtectedDictionary<string, ReferenceTableDescription, XElement> ReferenceTables { get; private set; }
        //Contains the set of tables being monitored for changes
        public ProtectedDictionary<string, TableDescription, XElement> Tables { get; private set; }
        
        //From config file
        // The public name for this connection.  This will be a parameter to the URL requesting the feed.
        public string Name { get; set; }
        // The amount of time in seconds to wait before checking to see if the database has changed.
        public int CacheDuration { get; set; }
        // Where to get the data for this connection
        public string ConnectionString
        {
            get
            {
                return _ConnectionString;
            }

            set
            {
                //TODO: Consider adding a ConnectionChanged event and putting the logic in a handler for the event.
                MaybeUpdateConnection(value);
                _ConnectionString = value;
            }
        }

        // The actual ADO.Net object that represents this connection to the database
        public SqlConnection Conn
        {
            get
            {
                return _conn;
            }
        }

        //The default value (in seconds) for how long to wait before checking to see if the database has
        //any changes.  Meantime, requests for changes get information from the cache of the 
        //last database change set.
        private const int DefaultCacheDuration = 5;

        //Initialize an instance from the XML element in the configuration file.
        public Connection(XElement connectionElement)
        {
            this.Tables = new ProtectedDictionary<string, TableDescription, XElement>(null);
            this.ReferenceTables = new ProtectedDictionary<string, ReferenceTableDescription, XElement>(null);
            this.Load(connectionElement);
        }

        //Helper function for above.
        public void Load(XElement connectionElement)
        {
            this.Name = AttributeValueOrNull(connectionElement, "Name");
            InitializeLocalMembers(connectionElement);

            //TODO: See if we can use the base class of ReferenceTableDescriptions to refactor to this code at some point
            IEnumerable<XElement> referenceTableElements = connectionElement.Elements(fm + "ReferenceTable");
            if (referenceTableElements != null)
            {
                ReferenceTables.LoadValues((XElement referenceTableElement) => AttributeValueOrNull(referenceTableElement, "Name"),
                    (XElement referenceTableElement) => new ReferenceTableDescription(this, referenceTableElement), referenceTableElements);
            }

            IEnumerable<XElement> tableElements = connectionElement.Elements(fm + "Table");
            if (tableElements != null)
            {
                Tables.LoadValues((XElement tableElement) => AttributeValueOrNull(tableElement, "Name"),
                    (XElement tableElement) => new TableDescription(this, tableElement), tableElements);

            }

        }

        //Updates the members of this class based on the relevant XML elements and attributes in the XML configuration
        //file.  This is called for new connections, and also to update existing connections when their definition
        //changes in the XML configuration file.
        public void InitializeLocalMembers(XElement connectionElement)
        {
            this.ConnectionString = AttributeValueOrNull(connectionElement, "ConnectionString");
            string newCacheDuration = AttributeValueOrNull(connectionElement, "CacheDuration");
            this.CacheDuration = (newCacheDuration == null) ? DefaultCacheDuration : int.Parse(newCacheDuration);
        }

        //This is invoked by FeedMap (see FeedMap.cs) to handle both new connections being added and existing 
        //connections being updated in the XML configuration file.
        public static Connection MergeConnection(XElement connectionElement, Connection currentConnection)
        {
            //If this is the first time we've encountered a connection with this name, create it.
            if (currentConnection == null)
                return new Connection(connectionElement);
            else
            {
                //Otherwise, with a write lock held, update the existing connection instance with the new 
                //settings in the XML configuration file
                currentConnection.ExecuteWithLock(LockType.Write, 
                    () => { currentConnection.InitializeLocalMembers(connectionElement); });

                //Then perform a similar merge operation on tables which are not monitored, but instead used
                //to provide additional information for titles and descriptions in the RSS feed.
                //TODO: See if we can use the base class of ReferenceTableDescriptions to refactor to this code at some point
                IEnumerable<XElement> referenceTableDescriptions = connectionElement.Elements(fm + "ReferenceTable");
                if (referenceTableDescriptions != null)
                {
                    currentConnection.ReferenceTables.MergeValues((XElement referenceTableElement) 
                        => AttributeValueOrNull(referenceTableElement, "Name"),
                        (referenceTableElement, currentTableDescription) 
                            => ReferenceTableDescription.MergeTableDescription(referenceTableElement, currentTableDescription, currentConnection),
                            referenceTableDescriptions);
                }

                //Also merge information about the tables we're monitoring
                IEnumerable<XElement> tableDescriptions = connectionElement.Elements(fm + "Table");
                if (tableDescriptions != null)
                {
                    currentConnection.Tables.MergeValues((XElement tableElement) => AttributeValueOrNull(tableElement, "Name"),
                        (tableElement, currentTableDescription) => TableDescription.MergeTableDescription(tableElement, currentTableDescription, currentConnection), 
                        tableDescriptions);
                }
                return currentConnection;
            }
        }

        /// <summary>
        /// Determine if the connection string for this connection is new or changed.  If it is,
        /// then close any existing ADO.NET connection object, and in any case create a new one.
        /// </summary>
        /// <param name="newConnectionString">Which database server and database to use, along with credential information.</param>
        private void MaybeUpdateConnection(string newConnectionString)
        {
            //If we haven't established a connection or the connection string has changed 
            if ((_ConnectionString == null) || (string.Compare(_ConnectionString, newConnectionString, 
                StringComparison.InvariantCultureIgnoreCase) != 0))
            {
                //If there is an existing connection, consider closing it
                if (_conn != null)
                {
                    if (_conn.State != ConnectionState.Closed) _conn.Close();
                    _conn = null;
                }

                //In any case, make a new connection
                _conn = new SqlConnection(newConnectionString);

            }
        }

        /// <summary>
        /// Get a syndication feed of the current changes (where "current" is defined by the XML configuration file) for
        /// the specified table.  Optimistically acquire a read lock but allow for an upgrade to a write lock if there
        /// are new changes received from the database.
        /// </summary>
        /// <param name="tableName">Which table to check for changes</param>
        /// <returns>A WCF syndication feed containing a description of the current set of changes.</returns>
        internal SyndicationFeed CurrentChanges(string tableName)
        {
            TableDescription table;
            if (Tables.TryLookupValue(tableName, out table))
            {
                //Optimistically take a read lock.  We'll have to back out of this lock and grab a write lock 
                //if there have been changes.  If we go for an upgradable lock every time we'll end up serializing all
                //reads on the lock.
                return table.ExecuteWithLock<SyndicationFeed>(LockType.Read, () => table.CurrentChanges());
            }
            else
                throw new ArgumentException(string.Format("Table {0} not found.", tableName), "tableName");
        }
    }
}
